using System;
using System.Text;
using System.Reflection;

namespace Open.Data.Persistence
{
	public class Query<T>: IQuery
    where T: class, IPersistable, new()
	{
		//Property variables
        private Type _type;
        private Type _activateType;
		
        private Conditions _where;
        private Conditions _having;
        private Columns _orders;
        private Columns _groupBy;
        private IQueries _subQueries;
        private IQuery _parent;
        private Columns _joinFrom;
        private Columns _joinTo;
        private bool _fill;

        private DataBinder<T> _binder;
        private JoinType _joinType;
        private QueryDataAdapter _adapter;

        //Constructors
		public Query()
		{
            _activateType = typeof(T);
            _type = typeof(T);
            _fill = true;
            _joinType = JoinType.Inner;

            //Sets PersistType to the viewing type, not the activating type
            //and creates the internal objects
            Initialize();
		}

        public Query(bool fill)
        {
            _activateType = typeof(T);
            _type = typeof(T);
            _fill = fill;
            _joinType = JoinType.Inner;

            //Sets mPersistType to the viewing type, not the activating type
            //and create the internal objects
            Initialize();
        }

		//Properties
		public virtual Type Type
		{
			get
			{
				return _type;
			}
		}

        public virtual Type ActivateType
        {
            get
            {
                return _activateType;
            }
        }

        public virtual bool Fill
        {
            get
            {
                return _fill;
            }
            set
            {
                _fill = value;
            }
        }

        public virtual DataBinder<T> Binder
        {
            get
            {
                return _binder;
            }
        }

        public virtual IQueries SubQueries
        {
            get
            {
                return _subQueries;
            }
            set
            {
                _subQueries = value;
            }
        }

		public virtual Conditions Where
		{
			get
			{
				return _where;
			}
		}

        public virtual Columns OrderBy
        {
            get
            {
                return _orders;
            }
        }

        public virtual Columns GroupBy
        {
            get
            {
                return _groupBy;
            }
            set
            {
                _groupBy = value;
            }
        }

        public virtual Conditions Having
        {
            get
            {
                return _having;
            }
            set
            {
                _having = value;
            }
        }

        public virtual IQuery Parent
        {
            get
            {
                return _parent;
            }
            set
            {
                _parent = value;
            }
        }

        public virtual Columns JoinFrom
        {
            get
            {
                return _joinFrom;
            }
            set
            {
                _joinFrom = value;
            }
        }

        public virtual Columns JoinTo
        {
            get
            {
                return _joinTo;
            }
            set
            {
                _joinTo = value;
            }
        }

        public virtual JoinType JoinType
        {
            get
            {
                return _joinType;
            }
            set
            {
                _joinType = value;
            }
        }

        public virtual T Item
        {
            get
            {
                if (Binder == null) throw new QueryException("Execute query before calling the Item property.");
                return Binder.Item;
            }
        }

		//Methods
        public virtual QueryDataAdapter Execute(Command command)
        {
            if (command == null) throw new ArgumentNullException("command");

            //Create data adapter from the command
            QueryDataAdapter adapter = command.Context.CreateQueryDataAdapter();

            //Add a data binder for each query with fill true
            IQuery query = this;
            query.ExecuteQuery(adapter);

            //Run the query by initialising the adapter
            adapter.Initialise();

            return adapter;
        }

        public virtual int Count(Command command)
        {
            if (command == null) throw new ArgumentNullException("command");
            return command.Count(this);
        }

        //Left joins
        public virtual void LeftJoin(IQuery query)
        {
            query.JoinType = JoinType.Left;
            Join(query);
        }

        public virtual void LeftJoin(IQuery query, string joinFrom, string joinTo)
        {
            query.JoinType = JoinType.Left;
            Join(query, joinFrom, joinTo);
        }

        public virtual void LeftJoin(IQuery query, string[] joinFrom, string[] joinTo)
        {
            query.JoinType = JoinType.Left;
            Join(query, joinFrom, joinTo);
        }

        public virtual void LeftJoin(IQuery query, IColumn joinFrom, IColumn joinTo)
        {
            query.JoinType = JoinType.Left;
            Join(query, joinFrom, joinTo);
        }

        public virtual void LeftJoin(IQuery query, Columns joinFrom, Columns joinTo)
        {
            query.JoinType = JoinType.Left;
            Join(query, joinFrom, joinTo);
        }

        public virtual void LeftJoin(IQuery query, PropertyInfo joinFrom, PropertyInfo joinTo)
        {
            LeftJoin(query, new Column(joinFrom), new Column(joinTo));
        }

        public virtual void LeftJoin(IQuery query, PropertyInfo[] joinFrom, PropertyInfo[] joinTo)
        {
            LeftJoin(query, new Columns(joinFrom), new Columns(joinTo));
        }

        //Right joins
        public virtual void RightJoin(IQuery query)
        {
            query.JoinType = JoinType.Right;
            Join(query);
        }

        public virtual void RightJoin(IQuery query, string joinFrom, string joinTo)
        {
            query.JoinType = JoinType.Right;
            Join(query, joinFrom, joinTo);
        }

        public virtual void RightJoin(IQuery query, string[] joinFrom, string[] joinTo)
        {
            query.JoinType = JoinType.Right;
            Join(query, joinFrom, joinTo);
        }

        public virtual void RightJoin(IQuery query, IColumn joinFrom, IColumn joinTo)
        {
            query.JoinType = JoinType.Right;
            Join(query, joinFrom, joinTo);
        }

        public virtual void RightJoin(IQuery query, Columns joinFrom, Columns joinTo)
        {
            query.JoinType = JoinType.Right;
            Join(query, joinFrom, joinTo);
        }

        public virtual void RightJoin(IQuery query, PropertyInfo joinFrom, PropertyInfo joinTo)
        {
            RightJoin(query, new Column(joinFrom), new Column(joinTo));
        }

        public virtual void RightJoin(IQuery query, PropertyInfo[] joinFrom, PropertyInfo[] joinTo)
        {
            RightJoin(query, new Columns(joinFrom), new Columns(joinTo));
        }

        //Inner joins
        public virtual void Join(IQuery query)
        {
            if (query.Parent != null) throw new QueryException(string.Format("This query has already been joined to a query of type {0}.",query.Parent.Type.FullName));
            query.Parent = this;

            //Attempt to join on the matching keys
            CacheItem from = Provider.Instance.ReflectionCache.Lookup(Type);
            CacheItem to = Provider.Instance.ReflectionCache.Lookup(query.Type);

            if ((from.HasPrimaryKey && to.HasPrimaryKey) || (from.HasCompoundKeys && to.HasCompoundKeys))
            {
                if (from.HasPrimaryKey)
                {
                    PropertyInfo fromprop = from.PrimaryKey;
                    PropertyInfo toprop = to.PrimaryKey;

                    if (fromprop.PropertyType != toprop.PropertyType) throw new QueryException("Primary key property types must be matching for " + fromprop.ToString() + " and " + toprop.ToString() + ".");

                    query.JoinFrom = new Columns(new PropertyInfo[] { fromprop });
                    query.JoinTo = new Columns(new PropertyInfo[] { toprop });
                }
                else
                {
                    PropertyInfo[] fromprops = from.CompoundKeys;
                    PropertyInfo[] toprops = to.CompoundKeys;

                    int i = 0;
                    foreach (PropertyInfo fromprop in fromprops)
                    {
                        PropertyInfo toprop = toprops[i];
                        if (fromprop.PropertyType != toprop.PropertyType) throw new QueryException("Compound key property types must be matching for " + fromprop.ToString() + " and " + toprop.ToString() + ".");
                        i++;
                    }

                    query.JoinFrom = new Columns(fromprops);
                    query.JoinTo = new Columns(toprops);
                }
            }
            else
            {
                throw new QueryException("Queries must have matching key types (primary or compound) to be joined automatically. Specify a From and To parameter to join these queries.");
            }


            SubQueries.Add(query);
        }

        public virtual void Join(IQuery query, string joinFrom, string joinTo)
        {
            if (query.Parent != null) throw new QueryException(string.Format("This query has already been joined to a query of type {0}.", query.Parent.Type.FullName));

            //Attempt to join on on the field names
            PropertyInfo from = ActivateType.GetProperty(joinFrom);
            if (from == null) throw new QueryException("Property " + joinFrom + " was not found for Type " + ActivateType.ToString() + ".");

            PropertyInfo to = query.ActivateType.GetProperty(joinTo);
            if (to == null) throw new QueryException("Property " + joinTo + " was not found for Type " + ActivateType.ToString() + ".");

            query.Parent = this;
            query.JoinFrom = new Columns(new PropertyInfo[] { from });
            query.JoinTo = new Columns(new PropertyInfo[] { to });
            SubQueries.Add(query);
        }

        public virtual void Join(IQuery query, string[] joinFrom, string[] joinTo)
        {
            if (query.Parent != null) throw new QueryException(string.Format("This query has already been joined to a query of type {0}.", query.Parent.Type.FullName));

            query.Parent = this;
            query.JoinFrom = new Columns(ActivateType, joinFrom);
            query.JoinTo = new Columns(query.ActivateType, joinTo);
            SubQueries.Add(query);
        }

        public virtual void Join(IQuery query, IColumn joinFrom, IColumn joinTo)
        {
            if (query.Parent != null) throw new QueryException(string.Format("This query has already been joined to a query of type {0}.", query.Parent.Type.FullName));

            //Validate
            if (joinFrom.PropertyInfo.ReflectedType != ActivateType) throw new QueryException("The joinFrom property is not a property of Type " + ActivateType.ToString() + ".");
            if (joinTo.PropertyInfo.ReflectedType != query.ActivateType) throw new QueryException("The joinTo property is not a property of Type " + query.ActivateType.ToString() + ".");

            query.Parent = this;
            query.JoinFrom = new Columns();
            query.JoinFrom.Add(joinFrom);
            query.JoinTo = new Columns();
            query.JoinTo.Add(joinTo);
            
            SubQueries.Add(query);
        }

        public virtual void Join(IQuery query, Columns joinFrom, Columns joinTo)
        {
            if (query.Parent != null) throw new QueryException(string.Format("This query has already been joined to a query of type {0}.", query.Parent.Type.FullName));

            //Validate
            foreach (IColumn field in joinFrom)
            {
                if (field.PropertyInfo.ReflectedType != ActivateType) throw new QueryException("A joinFrom property is not a property of Type " + ActivateType.ToString() + ".");
            }
            foreach (IColumn field in joinTo)
            {
                if (field.PropertyInfo.ReflectedType != query.ActivateType) throw new QueryException("A joinTo property is not a property of Type " + query.ActivateType.ToString() + ".");
            }

            query.Parent = this;
            query.JoinFrom = joinFrom;
            query.JoinTo = joinTo;
            SubQueries.Add(query);
        }

        public virtual void Join(IQuery query, PropertyInfo joinFrom, PropertyInfo joinTo)
        {
            Join(query, new Column(joinFrom), new Column(joinTo));
        }

        public virtual void Join(IQuery query, PropertyInfo[] joinFrom, PropertyInfo[] joinTo)
        {
            Join(query, new Columns(joinFrom), new Columns(joinTo));
        }

        public virtual DataBinder<T> GetBinder(QueryDataAdapter adapter)
        {
            if (adapter == null) throw new ArgumentNullException();
            if (_adapter != null && adapter != _adapter) throw new ArgumentException("Another adapter has already been bound to this query.");
            
            if (_binder == null)
            {
                _adapter = adapter;
                _binder = adapter.Command.Context.CreateDataBinder<T>(this);
                adapter.AddDataBinder<T>(_binder);
            }

            return _binder;
        }

        //Returns a column found in query type
        public virtual Column GetColumn(string name)
        {
            return new Column(typeof(T), name);
        }

        //Returns an ordered column found in query type
        public virtual Column GetOrder(string name)
        {
            return new Order(typeof(T), name);
        }

        //Returns an aggregate column found in query type
        public virtual Column GetAggregate(string name)
        {
            return new Aggregate(typeof(T), name);
        }

        public override int GetHashCode()
        {
            StringBuilder builder = new StringBuilder();
            AppendHashCode(builder);

            return builder.ToString().GetHashCode();
        }

        //Do not append a parent hash code as this will create a circular reference
        public virtual void AppendHashCode(StringBuilder builder)
        {
            //Append this types full name
            builder.Append(Type.FullName);

            //Append the join type
            builder.Append(JoinType.ToString());

            //Add an extra value if filling the recordset
            if (Fill) builder.Append(1);

            if (SubQueries != null) SubQueries.AppendHashCode(builder);
            if (Where != null) Where.AppendHashCode(builder);
            if (OrderBy != null) OrderBy.AppendHashCode(builder);
            if (GroupBy != null) GroupBy.AppendHashCode(builder);
            if (Having != null) Having.AppendHashCode(builder);

            //Add sub query hashes
            if (SubQueries != null && SubQueries.Count > 0)
            {
                foreach (IQuery subquery in SubQueries)
                {
                    builder.Append(subquery.GetHashCode());
                }
            }
        }

        private void Initialize()
        {
            //Get the underlying persisted type, if relevant (eg a view)
            _type = Open.Data.Persistence.Common.GetPersistType(_type);

            //Create child objects
            _subQueries = new Queries();
            _where = new Conditions();
            _orders = new Columns();
            _groupBy = new Columns();
        }

        void IQuery.ExecuteQuery(QueryDataAdapter adapter)
        {
            if (Fill) GetBinder(adapter);

            //Create binders for each sub query
            foreach (IQuery query in SubQueries)
            {
                query.ExecuteQuery(adapter);
            }
        }
    }
}
